// Assembler for the Parallax Propeller
//
// The job we're doing is pretty simple, so we don't bother with yacc and lex.
// We read in the entire file, tokenize it, turn it into a series of statements
// to be translated, and then perform translation and output.
//

#include <getopt.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "common.h"

#include "propasm-tbl.h"

#define IDENT_CHAR(x) ((x >= 'a' && x <= 'z') || (x >= 'A' && x <= 'Z'))
#define MAX_TOKLEN 60

/*
 * The valid token types.
 */
enum tok_type { COMMENT, IDENT, NUM, WS, NL, COMMA };

/*
 * A single token
 */
typedef struct {
	enum tok_type type;
	union {
		char *sval;
		int ival;
	} data;
	int line;
} token_t;

/*
 * A single command
 */
typedef struct {
	char *ident;
	list_t *args;
} instruction_t;

/*
 * A label
 */
typedef struct {
	char *name;		// the name of the label
	struct lnode *next;	// the next node in the program
} label_t;

/*
 * Represents a complete assembly program.
 */
typedef struct {
	hashmap_t *labels;
	list_t *instructions;
} program_t;

hashmap_t *opcodes;

void print_usage();
void print_help();

// Initialize global data to desired values.
void initialize();

char *read_file(FILE *, char *);
list_t *tokenize(char *);
program_t *parse(list_t *);
void output_program(program_t *, FILE *);

// Print the times collected in the stopwatch.
void print_times();

// Debugging method to print a given list of tokens.
void print_token_list(list_t *);

char *xname;
int show_times = 0;

stopwatch_t *times;

int main(int argc, char *argv[]) {
	char *ofname = NULL; // output filename

	// start the stopwatch
	times = stopwatch_create();
	stopwatch_start(times);

	xname = argv[0];
	char *cvalue = NULL;
	int index;
	int c;

	opterr = 0;

	while ((c = getopt (argc, argv, "hTo:")) != -1)
		switch (c) {
		case 'T':
			show_times = 1;
			break;
		case 'h':
			print_help();
			return 0;
		case 'o':
			ofname = optarg;
			break;
		case '?':
			switch (optopt) {
			case 'o':
				ferrf("-%c: argument required", optopt);
				break;
			default:
				ferrf("-%c: unknown option", optopt);
			}
		default:
			ferrf("unknown error (%s:%d)", __FILE__, __LINE__);
		}

	stopwatch_mark(times, "ARGS");

	initialize();
	stopwatch_mark(times, "INITIALIZE");

	stopwatch_pause(times);
	FILE *input;
	FILE *output = stdout;

	int arg_count = argc-optind;
	if (arg_count > 1)
		ferr("too many options");
	else if (arg_count == 1) {
		input = fopen(argv[optind], "r");
		if (!input)
			ferrf("%s: error opening file", argv[optind]);
	} else // no arguments given - read from stdin
		input = stdin;

	if (ofname) {
		output = fopen(ofname, "w");
		if (!output)
			ferrf("%s: error opening file", ofname);
	}

	char *program_string = read_file(input, ((arg_count==1) ? argv[optind] : "<stdin>"));
	fclose(input);

	stopwatch_unpause(times);
	list_t *tokens = tokenize(program_string);
	stopwatch_mark(times, "TOKENIZE");
	program_t *program = parse(tokens);
	stopwatch_mark(times, "PARSE");
	output_program(program, output);
	
	fclose(output);

	if (show_times)
		print_times();

	return 0;
}

void print_usage() {
	// TODO print some usage information
}

void print_help() {
	// TODO actually print decent help
	print_usage();
}

int opcode_hash(void *vstr) {
	char *str = vstr;
	return strlen(str) + 4*str[0] + 26*str[1];
}

void initialize() {
	opcodes = make_hashmap(103);
	hashmap_hash(opcodes, opcode_hash);
	int i;
	for (i = 0; i < 077; i++)
		hashmap_set(opcodes, propeller_assembly[i].instruction, &propeller_assembly[i]);
}

char *read_file(FILE *file, char *filename) {
	int sz = 8, len = 0;
	char *prog = (char *)malloc(sz);
	char c = fgetc(file);
	while (!feof(file)) {
		if (ferror(file))
			ferrf("%s: error reading file", filename);
		if (len+2 > sz) {
			sz *= 2;
			char *oldprog = prog;
			prog = (char *)malloc(sz);
			strncpy(prog, oldprog, len+1);
			free(oldprog);
		}
		prog[len++] = c;
		c=fgetc(file);
	}
	prog[len] = '\0';
	return prog;
}

list_t *tokenize(char *prog) {
	list_t *token_list = make_list();
	int tok_buf_pos = 0;
	char c;
	int prog_pos = 0;
	int line_no = 1;
	c = prog[prog_pos++];

	while (c != '\0') {
		token_t *tok = malloc(sizeof(token_t));
		tok->line = line_no;
		if (c == '\n') { // newline
			tok->type = NL;
			c = prog[prog_pos++];
		} else if (c == ' ' || c == '\t') { // whitespace
			tok->type = WS;
			c = prog[prog_pos++];
		} else if (c == '\'') { // comment
			tok->type = COMMENT;
			while (c != '\n' && c != '\0')
				c = prog[prog_pos++];
		} else if (c == ',') { // comma
			tok->type = COMMA;
			c = prog[prog_pos++];
		} else if (c >= '0' && c <= '9') { // number
			char *tok_buf = calloc(MAX_TOKLEN, sizeof(char));
			tok->type = NUM;
			while (c >= '0' && c <= '9') {
				tok_buf[tok_buf_pos++] = c;
				if(tok_buf_pos >= MAX_TOKLEN)
					ferrf("token too long at line %d (maximum length is %d)", line_no, MAX_TOKLEN);
				c = prog[prog_pos++];
			}
			tok->data.ival = atoi(tok_buf);
		} else if (IDENT_CHAR(c)) { // identifier
			char *tok_buf = calloc(MAX_TOKLEN, sizeof(char));
			tok->type = IDENT;
			while (IDENT_CHAR(c) || (c >= '0' && c <= '9')) {
				tok_buf[tok_buf_pos++] = c;
				if(tok_buf_pos >= MAX_TOKLEN)
					ferrf("token too long at line %d (maximum length is %d)", line_no, MAX_TOKLEN);
				c = prog[prog_pos++];
			}
			tok->data.sval = tok_buf;
		} else
			ferrf("invalid token at line %d", line_no);

		if (tok->type == NL)
			line_no++;

		append_list(token_list, tok);
		tok_buf_pos = 0;
	}

	return token_list;
}

void print_token_list(list_t *token_list) {
	list_iter_t *iter = list_get_iter(token_list);
	while (list_iter_has_next(iter)) {
		token_t *tok = list_iter_next(iter);
		switch (tok->type) {
		case IDENT:
			printf("ident: %s\n", tok->data.sval);
			break;
		case COMMENT:
			puts("comment");
			break;
		case NUM:
			puts("num");
			break;
		case WS:
			// do nothing
			break;
		case NL:
			// do nothing
			break;
		case COMMA:
			puts("comma");
		}
	}
	list_iter_destroy(iter);
}

// read until newline (or end) from source, appending elements to dest
void read_until_newline(list_iter_t *from, list_t *to) {
	token_t *tok;
	while (list_iter_has_next(from) && (tok = list_iter_next(from))->type != NL) {
		append_list(to, tok);
	}
}

program_t *parse(list_t *tokens) {
	program_t *program = malloc(sizeof(program_t));
	program->instructions = make_list();
	program->labels = make_hashmap(100);

	label_t *last_label = NULL;
	list_iter_t *iter = list_get_iter(tokens);
	while (list_iter_has_next(iter)) {
		token_t *tok = list_iter_next(iter);
		int indented = 0;
		// Begin reading a statement
		switch (tok->type) {
			case IDENT:
				if (indented) {
					// this is a label
					label_t *label = malloc(sizeof(label_t));
					label->name = tok->data.sval;
					label->next = NULL;
					hashmap_set(program->labels, label->name, label);
					// we'll mark the next command we come across
					last_label = label;
				} else {
					// this is some sort of instruction
					instruction_t *instr = malloc(sizeof(instruction_t));
					instr->ident = tok->data.sval;
					// collect the args
					instr->args = make_list();
					read_until_newline(iter, instr->args);
					append_list(program->instructions, instr);
				}
				break;
			case NUM:
				// Numbers can't begin statements - error out
				ferrf("command may not begin with number at line %d\n", tok->line);
				break;
			case WS:
				// flip the indented flag but don't try to read
				// the statement
				indented = 1;
				break;
			case COMMA:
				ferrf("command may not begin with comma at line %d\n", tok->line);
				break;
		}
	}
	list_iter_destroy(iter);
	return program;
}

// translate and output the program to the given file
void output_program(program_t *program, FILE *output) {
	// traverse the program tree
	list_iter_t *iter = list_get_iter(program->instructions);
	while (list_iter_has_next(iter)) {
		instruction_t *instr = list_iter_next(iter);
	}
	list_iter_destroy(iter);
}

// print collected timings to standard error
void print_times() {
	list_iter_t *iter = list_get_iter(times->marks);
	while (list_iter_has_next(iter)) {
		struct mark *m = list_iter_next(iter);
		fprintf(stderr, "%s : %d\n", m->name, (int)stopwatch_get_delay(times, m->name));
	}
	list_iter_destroy(iter);
}

